Fix per-request DbService scoping in cloud API#475
Merged
RhysSullivan merged 1 commit intomainfrom May 2, 2026
Merged
Conversation
Cloudflare Workers forbids reusing I/O objects across request handlers. The v4 routing refactor (#468) collapsed per-request layers into `Layer.provideMerge` of an `HttpRouter.toWebHandler` app, which builds the layer ONCE at worker boot. The postgres.js socket (`Writable`) opened during request 1 was then unusable from request 2 — every protected endpoint 500ed with "Cannot perform I/O on behalf of a different request". `HttpRouter.provideRequest` doesn't actually rebuild per request despite the name (its `Layer.build` runs in the OUTER middleware effect at layer-construction time). The only primitive that does is a custom middleware whose per-request handler calls `Layer.build` inside `Effect.scoped`. New `requestScopedMiddleware` helper does that. `ExecutionStackMiddleware` no longer captures DbService/UserStoreService at layer-time; it `.combine(...)`s with `requestScopedMiddleware(rsLive)` so its `requires` collapses to never. Sub-API factories (`makeNonProtectedApiLive`/`makeOrgApiLive`/`makeProtectedApiLive`) thread the per-request layer through, and `makeApiLive(rsLive)` lets tests substitute a counting fake for `DbService.Live`. Regression coverage in `api.request-scope.node.test.ts` pins down four cases: `Layer.provideMerge` (bug, captures boot scope), `HttpRouter.provideRequest` (also boot scope despite the name), `requestScopedMiddleware` (fix primitive), and `makeApiLive` (prod factory) — verified to fail when the wiring reverts to `provideMerge`.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
[storage-drizzle] findMany select failed: Cannot perform I/O on behalf of a different request. (I/O type: Writable)(SentryNODE-CLOUDFLARE-WORKERS-31, first deploy 2026-05-02T05:05:09Z).Layer.provideMergeof anHttpRouter.toWebHandlerapp.toWebHandlerbuilds the layer ONCE at worker boot, so the postgres.js socket (aWritable) opened during request 1 became unusable from request 2.requestScopedMiddleware(layer)whose per-request handler doesLayer.build(layer)insideEffect.scoped— the only primitive that actually rebuilds per request (HttpRouter.provideRequestdoes NOT despite the name).ExecutionStackMiddleware.combine(...)s with it sorequires: DbService | UserStoreServicecollapses tonever. Sub-API factories thread the per-request layer through, andmakeApiLive(rsLive)lets tests substitute a counting fake.Test plan
apps/cloud/src/api.request-scope.node.test.ts(4 cases):Layer.provideMergecaptures boot scope (bug);HttpRouter.provideRequestalso captures boot scope (misleading name);requestScopedMiddlewarerebuilds per request;makeApiLive(prod factory) rebuilds per request — verified to fail (acquires: 1) when the wiring reverts toprovideMergeand pass (acquires: 2) with the fix./api/scopes/*/sourcesboth succeed.